package xcron

import (
	"errors"
	"github.com/robfig/cron/v3"
	"slices"
	"sync"
)

var (
	ErrKeyNil     = errors.New("key nil")
	ErrSpecNil    = errors.New("spec nil")
	ErrKeyExisted = errors.New("scheduled tasks already exist")
	ErrCmdFuncNil = errors.New("cmd func nil")
)

type Cron struct {
	client  *cron.Cron
	entries []*Entry
	mux     sync.Mutex
}

func New(logger Logger) *Cron {
	c := cron.New(
		cron.WithSeconds(),
		cron.WithLogger(logger),
		cron.WithChain(cron.Recover(logger)),
	)

	return &Cron{
		client:  c,
		entries: make([]*Entry, 0),
	}
}

func (c *Cron) Start() {
	c.client.Start()
}

func (c *Cron) Stop() {
	c.client.Stop()
}

func (c *Cron) Client() *cron.Cron {
	return c.client
}

func (c *Cron) Has(key string) bool {
	return slices.ContainsFunc(c.Entries(), func(item *Entry) bool {
		return item.Key == key
	})
}

func (c *Cron) Entries() []*Entry {
	c.mux.Lock()
	defer c.mux.Unlock()
	return c.entries
}

func (c *Cron) Entry(key string) *Entry {
	entries := c.Entries()
	index := slices.IndexFunc(entries, func(item *Entry) bool {
		return item.Key == key
	})
	if index > -1 {
		return entries[index]
	}
	return nil
}

// Remove an entry from being run in the future.
func (c *Cron) Remove(key string) {
	c.mux.Lock()
	defer c.mux.Unlock()
	entries := make([]*Entry, 0)
	for _, e := range c.entries {
		if e.Key != key {
			entries = append(entries, e)
		} else {
			c.client.Remove(e.entry.ID)
		}
	}

	c.entries = entries
}

func (c *Cron) ExecJob(key string) error {
	entry := c.Entry(key)
	if entry == nil {
		return errors.New("未查找到任务")
	}
	entry.Run()
	return nil
}

func (c *Cron) AddJob(spec string, cmd Job) (*Entry, error) {
	if cmd.Key() == "" {
		return nil, ErrKeyNil
	}

	if spec == "" {
		return nil, ErrSpecNil
	}

	if c.Has(cmd.Key()) {
		return nil, ErrKeyExisted
	}
	id, err := c.client.AddJob(spec, cmd)
	if err != nil {
		return nil, err
	}
	entry := c.client.Entry(id)
	e := &Entry{
		entry: entry,
		cmd:   cmd,
		ID:    entry.ID,
		Spec:  spec,
		Key:   cmd.Key(),
		Name:  cmd.Name(),
		Desc:  cmd.Desc(),
		Next:  entry.Next,
		Prev:  entry.Prev,
	}
	c.entries = append(c.entries, e)
	return e, err
}

func (c *Cron) AddFunc(spec string, cmd func(), key, name, desc string) (*Entry, error) {
	if cmd == nil {
		return nil, ErrCmdFuncNil
	}
	job := &FuncJob{}
	job.SetKey(key)
	job.SetName(name)
	job.SetDesc(desc)
	job.SetJob(cmd)
	return c.AddJob(spec, job)
}
